﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Windows.Input;
using JetBrains.Annotations;
using MugenMvvmToolkit.Annotations;
using MugenMvvmToolkit.Infrastructure;
using MugenMvvmToolkit.Interfaces;
using MugenMvvmToolkit.Interfaces.ViewModels;
using MugenMvvmToolkit.Models;
using MugenMvvmToolkit.Utils;

namespace MugenMvvmToolkit.ViewModels
{
    /// <summary>
    ///     Represents the default implementation of <see cref="IRelayCommand" />.
    /// </summary>
    public class RelayCommand : IRelayCommand, IHandler<IStateChangedMessage>
    {
        #region Fields

        internal readonly Predicate<object> CanExecuteDelegate;
        internal readonly Action<object> ExecuteDelegate;
        private readonly WeakReferenceCollection<object> _notifiers;
        private readonly Dictionary<WeakReference, PropertyChangedEventHandler> _handlers;

        private IThreadManager _threadManager;
        private event EventHandler CanExecuteChangedInternal;
        private int _suspendCount;

        #endregion

        #region Constructors

        /// <summary>
        ///     Initializes a new instance of the <see cref="RelayCommand" /> class.
        /// </summary>
        /// <param name="execute">The specified command action for execute.</param>
        /// <param name="executionMode">
        ///     Specifies the execution mode for <c>RaiseCanExecuteChanged</c> method.
        /// </param>
        /// <param name="threadManager">
        ///     The specified <see cref="IThreadManager" />.
        /// </param>
        public RelayCommand([NotNull]Action<object> execute, ExecutionType? executionMode = null, IThreadManager threadManager = null)
            : this(execute, null, executionMode, threadManager, null)
        {
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="RelayCommand" /> class.
        /// </summary>
        /// <param name="execute">The specified command action for execute.</param>
        /// <param name="canExecute">The specified command condition.</param>
        /// <param name="notifiers">
        ///     The specified object implemented for auto call event CanExecuteChanged. If you change the properties of the objects to be caused by an event CanExecuteChanged.
        /// </param>
        public RelayCommand([NotNull]Action<object> execute, [NotNull]Predicate<object> canExecute, [NotEmptyParams] params object[] notifiers)
            : this(execute, canExecute, null, null, notifiers)
        {
            Should.NotBeNull(canExecute, "canExecute");
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="RelayCommand" /> class.
        /// </summary>
        /// <param name="execute">The specified command action for execute.</param>
        /// <param name="canExecute">The specified command condition.</param>
        /// <param name="executionMode">
        ///     Specifies the execution mode for <c>RaiseCanExecuteChanged</c> method.
        /// </param>
        /// <param name="threadManager">
        ///     The specified <see cref="IThreadManager" />.
        /// </param>
        /// <param name="notifiers">
        ///     The specified object implemented for auto call event CanExecuteChanged. If you change the properties of the objects to be caused by an event CanExecuteChanged.
        /// </param>
        public RelayCommand([NotNull]Action<object> execute, [CanBeNull]Predicate<object> canExecute, ExecutionType? executionMode = null, IThreadManager threadManager = null,
                            [NotEmptyParams]params object[] notifiers)
        {
            Should.NotBeNull(execute, "execute");
            if (executionMode == null)
                executionMode = GlobalSettings.CommandCanExecuteExecutionMode;
            if (threadManager == null)
                threadManager = GlobalSettings.DefaultThreadManager;

            if (GlobalSettings.UseWeakDelegate)
            {
                ExecuteDelegate = MvvmUtils.ConvertToWeakDelegate(execute);
                if (canExecute != null)
                    CanExecuteDelegate = MvvmUtils.ConvertToWeakDelegate(canExecute);
            }
            else
            {
                ExecuteDelegate = execute;
                CanExecuteDelegate = canExecute;
            }
            _threadManager = threadManager;
            CanExecuteMode = executionMode.Value;
            ExecutionMode = GlobalSettings.CommandExecutionMode;
#if WPF
            UseCommandManager = GlobalSettings.UseCommandManager;
#endif
            if (CanExecuteDelegate == null) return;

            _notifiers = new WeakReferenceCollection<object>();
            _handlers = new Dictionary<WeakReference, PropertyChangedEventHandler>();
            if (notifiers == null) return;
            foreach (var item in notifiers)
            {
                // ReSharper disable DoNotCallOverridableMethodsInConstructor
                AddNotifier(item);
                // ReSharper restore DoNotCallOverridableMethodsInConstructor
            }
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets a value indicating whether change to the command is made when its notifications are suspended.
        /// </summary>
        protected bool IsNotificationsDirty { get; set; }

        /// <summary>
        /// Gets a value indicating whether the command should add event even if canExecute delegate is null.
        /// </summary>
        protected virtual bool AddEventIfCanExecuteNull
        {
            get { return false; }
        }

        /// <summary>
        ///     Gets or sets the <see cref="IThreadManager" />.
        /// </summary>
        public IThreadManager ThreadManager
        {
            get
            {
                if (_threadManager == null)
                    return GlobalSettings.DefaultThreadManager;
                return _threadManager;
            }
            set { _threadManager = value; }
        }

        #endregion

        #region Methods

        private void OnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
        {
            RaiseCanExecuteChanged();
        }

        private void EndSuspendNotifications()
        {
            Interlocked.Decrement(ref _suspendCount);
            if (_suspendCount != 0 || !IsNotificationsDirty) return;
            IsNotificationsDirty = false;
            RaiseCanExecuteChanged();
        }

        #endregion

        #region Implementation of interfaces

        /// <summary>
        ///     Defines the method to be called when the command is invoked.
        /// </summary>
        /// <param name="parameter">Data used by the command.  If the command does not require data to be passed, this object can be set to null.</param>
#if !DEBUG
        [DebuggerStepThrough]
#endif
        public virtual void Execute(object parameter)
        {
            switch (ExecutionMode)
            {
                case CommandExecutionType.CanExecuteBeforeExecute:
                    if (!CanExecute(parameter))
                        return;
                    break;
                case CommandExecutionType.CanExecuteBeforeExecuteWithException:
                    if (!CanExecute(parameter))
                        Thrower.CommandCannotBeExecuted();
                    break;
            }
            ExecuteDelegate(parameter);
        }

        /// <summary>
        ///     Defines the method that determines whether the command can execute in its current state.
        /// </summary>
        /// <returns>
        ///     true if this command can be executed; otherwise, false.
        /// </returns>
        /// <param name="parameter">Data used by the command.  If the command does not require data to be passed, this object can be set to null.</param>
#if !DEBUG
        [DebuggerStepThrough]
#endif
        public virtual bool CanExecute(object parameter)
        {
            return CanExecuteDelegate == null || CanExecuteDelegate(parameter);
        }

        /// <summary>
        ///     Occurs when changes occur that affect whether or not the command should execute.
        /// </summary>
        public virtual event EventHandler CanExecuteChanged
        {
            [DebuggerStepThrough]
            add
            {
                if (!AddEventIfCanExecuteNull && CanExecuteDelegate == null) return;
#if WPF
                if (UseCommandManager)
                    ThreadManager.InvokeInUi(() => CommandManager.RequerySuggested += value);
#endif
                CanExecuteChangedInternal += value;
            }
            [DebuggerStepThrough]
            remove
            {
#if WPF
                if (UseCommandManager)
                    ThreadManager.InvokeInUi(() => CommandManager.RequerySuggested -= value);
#endif
                CanExecuteChangedInternal -= value;
            }
        }

#if WPF
        /// <summary>
        /// Enabling subscribe to CommandManager.RequerySuggested event.
        /// </summary>
        public bool UseCommandManager { get; set; }

#endif

        /// <summary>
        ///     Specifies the execution mode for <c>Execute</c> method.
        /// </summary>
        public CommandExecutionType ExecutionMode { get; set; }

        /// <summary>
        /// Specifies the execution mode for <c>RaiseCanExecuteChanged</c> method in <c>IRelayCommand</c>.
        /// </summary>
        public ExecutionType CanExecuteMode { get; set; }

        /// <summary>
        ///     Gets the collection of listeners.
        /// </summary>
        public IEnumerable<object> Notifiers
        {
            get
            {
                if (CanExecuteDelegate == null)
                    return Enumerable.Empty<object>();
                return _notifiers.ToArray();
            }
        }

        /// <summary>
        ///     Adds the specified notifier to manage the <c>CanExecuteChanged</c> event.
        /// </summary>
        /// <param name="item">The specified notifier item.</param>
        public virtual bool AddNotifier(object item)
        {
            Should.NotBeNull(item, "item");
            if (CanExecuteDelegate == null) return false;
            using (DeadLockDetector.Lock(_notifiers))
            {
                if (_notifiers.Contains(item))
                    return false;
                var observable = item as IObservable;
                if (observable != null && observable.Subscribe(this))
                {
                    _notifiers.Add(item);
                    return true;
                }

                var propertyChanged = item as INotifyPropertyChanged;
                if (propertyChanged != null)
                {
                    var weakReference = _notifiers.Add(propertyChanged);
                    var eventHandler = MvvmUtils.ConvertToWeakDelegate(new PropertyChangedEventHandler(OnPropertyChanged));
                    _handlers[weakReference] = eventHandler;
                    propertyChanged.PropertyChanged += eventHandler;
                    return true;
                }

                return false;
            }
        }

        /// <summary>
        ///     Removes the specified notifier.
        /// </summary>
        /// <param name="item">The specified notifier item.</param>
        public virtual bool RemoveNotifier(object item)
        {
            Should.NotBeNull(item, "item");
            if (CanExecuteDelegate == null) return false;
            using (DeadLockDetector.Lock(_notifiers))
            {
                if (!_notifiers.Contains(item))
                    return false;
                var observable = item as IObservable;
                if (observable != null)
                    observable.Unsubscribe(this);

                var propertyChanged = item as INotifyPropertyChanged;
                if (propertyChanged != null)
                {
                    var weakReference = _notifiers.FindWeakReference(item);
                    PropertyChangedEventHandler handler;
                    if (weakReference != null && _handlers.TryGetValue(weakReference, out handler) && handler != null)
                        propertyChanged.PropertyChanged -= handler;
                }
                return _notifiers.Remove(item);
            }
        }

        /// <summary>
        /// Removes all listeners.
        /// </summary>
        public virtual void ClearNotifiers()
        {
            if (CanExecuteDelegate == null) return;
            object[] objects;
            using (DeadLockDetector.Lock(_notifiers))
                objects = _notifiers.ToArray();
            foreach (var notifier in objects)
                RemoveNotifier(notifier);
        }

        /// <summary>
        ///     This method can be used to raise the CanExecuteChanged handler.
        /// </summary>
        public virtual void RaiseCanExecuteChanged()
        {
            if (IsNotificationsSuspended)
            {
                IsNotificationsDirty = true;
                return;
            }
            EventHandler executeChangedInternal = CanExecuteChangedInternal;
            if (executeChangedInternal == null) return;
            CanExecuteMode.Invoke(ThreadManager, () => executeChangedInternal(this, EventArgs.Empty));
        }

        /// <summary>
        ///     Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            var canExecuteChangedInternal = CanExecuteChangedInternal;
            if (canExecuteChangedInternal != null)
            {
                foreach (EventHandler eventHandler in canExecuteChangedInternal.GetInvocationList())
                    CanExecuteChanged -= eventHandler;
            }
            ClearNotifiers();
        }

        /// <summary>
        ///     Handles the message.
        /// </summary>
        /// <param name="sender">The object that raised the event.</param>
        /// <param name="message">Information about event.</param>
        void IHandler<IStateChangedMessage>.Handle(object sender, IStateChangedMessage message)
        {
            RaiseCanExecuteChanged();
        }

        /// <summary>
        ///     Gets or sets a value indicating whether change notifications are suspended. <c>True</c> if notifications are suspended, otherwise, <c>false</c>.
        /// </summary>
        public virtual bool IsNotificationsSuspended
        {
            get { return _suspendCount != 0; }
        }

        /// <summary>
        ///     Suspends the change notifications until the returned <see cref="IDisposable" /> is disposed.
        /// </summary>
        /// <returns>An instance of token.</returns>
        public virtual IDisposable SuspendNotifications()
        {
            Interlocked.Increment(ref _suspendCount);
            return new ActionToken(EndSuspendNotifications);
        }

        #endregion
    }
}